<!DOCTYPE HTML>
<html lang="en" class="sidebar-visible no-js light">
    <head>
        <!-- Book generated using mdBook -->
        <meta charset="UTF-8">
        <title>Raft共识算法（三）——日志备份（log replication） - 读论文</title>


        <!-- Custom HTML head -->
        
        <meta content="text/html; charset=utf-8" http-equiv="Content-Type">
        <meta name="description" content="">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <meta name="theme-color" content="#ffffff" />

        <link rel="icon" href="favicon.svg">
        <link rel="shortcut icon" href="favicon.png">
        <link rel="stylesheet" href="css/variables.css">
        <link rel="stylesheet" href="css/general.css">
        <link rel="stylesheet" href="css/chrome.css">
        <link rel="stylesheet" href="css/print.css" media="print">

        <!-- Fonts -->
        <link rel="stylesheet" href="FontAwesome/css/font-awesome.css">
        <link rel="stylesheet" href="fonts/fonts.css">

        <!-- Highlight.js Stylesheets -->
        <link rel="stylesheet" href="highlight.css">
        <link rel="stylesheet" href="tomorrow-night.css">
        <link rel="stylesheet" href="ayu-highlight.css">

        <!-- Custom theme stylesheets -->

        <!-- MathJax -->
        <script async type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.1/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script>
    </head>
    <body>
        <!-- Provide site root to javascript -->
        <script type="text/javascript">
            var path_to_root = "";
            var default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "navy" : "light";
        </script>

        <!-- Work around some values being stored in localStorage wrapped in quotes -->
        <script type="text/javascript">
            try {
                var theme = localStorage.getItem('mdbook-theme');
                var sidebar = localStorage.getItem('mdbook-sidebar');

                if (theme.startsWith('"') && theme.endsWith('"')) {
                    localStorage.setItem('mdbook-theme', theme.slice(1, theme.length - 1));
                }

                if (sidebar.startsWith('"') && sidebar.endsWith('"')) {
                    localStorage.setItem('mdbook-sidebar', sidebar.slice(1, sidebar.length - 1));
                }
            } catch (e) { }
        </script>

        <!-- Set the theme before any content is loaded, prevents flash -->
        <script type="text/javascript">
            var theme;
            try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
            if (theme === null || theme === undefined) { theme = default_theme; }
            var html = document.querySelector('html');
            html.classList.remove('no-js')
            html.classList.remove('light')
            html.classList.add(theme);
            html.classList.add('js');
        </script>

        <!-- Hide / unhide sidebar before it is displayed -->
        <script type="text/javascript">
            var html = document.querySelector('html');
            var sidebar = 'hidden';
            if (document.body.clientWidth >= 1080) {
                try { sidebar = localStorage.getItem('mdbook-sidebar'); } catch(e) { }
                sidebar = sidebar || 'visible';
            }
            html.classList.remove('sidebar-visible');
            html.classList.add("sidebar-" + sidebar);
        </script>

        <nav id="sidebar" class="sidebar" aria-label="Table of contents">
            <div class="sidebar-scrollbox">
                <ol class="chapter"><li class="chapter-item expanded affix "><a href="chapter_1.html">读论文活动</a></li><li class="chapter-item expanded affix "><li class="part-title">6.824 分布式系统</li><li class="chapter-item expanded "><a href="Mapreduce.html"><strong aria-hidden="true">1.</strong> Mapreduce</a></li><li class="chapter-item expanded "><a href="GFS.html"><strong aria-hidden="true">2.</strong> GFS</a></li><li class="chapter-item expanded "><a href="VM-FT.html"><strong aria-hidden="true">3.</strong> VM-FT</a></li><li class="chapter-item expanded "><a href="Raft.html"><strong aria-hidden="true">4.</strong> Raft</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="Raft0.html"><strong aria-hidden="true">4.1.</strong> 感性认识Raft</a></li><li class="chapter-item expanded "><a href="Raft1.html"><strong aria-hidden="true">4.2.</strong> 什么是Raft？</a></li><li class="chapter-item expanded "><a href="Raft2.html"><strong aria-hidden="true">4.3.</strong> 复制状态机（Replicated State Machine）</a></li><li class="chapter-item expanded "><a href="Raft3.html"><strong aria-hidden="true">4.4.</strong> What's wrong with Paxos?</a></li><li class="chapter-item expanded "><a href="Raft4.html"><strong aria-hidden="true">4.5.</strong> 向可理解性进军</a></li><li class="chapter-item expanded "><a href="Raft5.html"><strong aria-hidden="true">4.6.</strong> Raft共识算法（零）</a></li><li class="chapter-item expanded "><a href="Raft6.html"><strong aria-hidden="true">4.7.</strong> Raft共识算法（一）——基础概念</a></li><li class="chapter-item expanded "><a href="Raft7.html"><strong aria-hidden="true">4.8.</strong> Raft共识算法（二）——选举leader</a></li><li class="chapter-item expanded "><a href="Raft8.html" class="active"><strong aria-hidden="true">4.9.</strong> Raft共识算法（三）——日志备份（log replication）</a></li><li class="chapter-item expanded "><a href="Raft9.html"><strong aria-hidden="true">4.10.</strong> Raft共识算法（四）——安全性和选举限制</a></li><li class="chapter-item expanded "><a href="Raft10.html"><strong aria-hidden="true">4.11.</strong> Raft共识算法（五）——如何提交之前term里的entry</a></li><li class="chapter-item expanded "><a href="Raft11.html"><strong aria-hidden="true">4.12.</strong> Raft共识算法（六）——安全性定理</a></li><li class="chapter-item expanded "><a href="Raft12.html"><strong aria-hidden="true">4.13.</strong> Raft共识算法（七）——如果follower/candidate宕机了</a></li><li class="chapter-item expanded "><a href="Raft13.html"><strong aria-hidden="true">4.14.</strong> Raft共识算法（八）——时间与可用性</a></li><li class="chapter-item expanded "><a href="Raft14.html"><strong aria-hidden="true">4.15.</strong> 成员变更</a></li><li class="chapter-item expanded "><a href="Raft15.html"><strong aria-hidden="true">4.16.</strong> 日志压缩</a></li><li class="chapter-item expanded "><a href="Raft16.html"><strong aria-hidden="true">4.17.</strong> 与Client的交互</a></li><li class="chapter-item expanded "><a href="Raft17.html"><strong aria-hidden="true">4.18.</strong> 实验时遇到的bug</a></li><li class="chapter-item expanded "><a href="Raft18.html"><strong aria-hidden="true">4.19.</strong> 总结</a></li></ol></li><li class="chapter-item expanded "><a href="Zookeeper.html"><strong aria-hidden="true">5.</strong> Zookeeper</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="linearizability1.html"><strong aria-hidden="true">5.1.</strong> 线性一致性（一）——基础概念</a></li><li class="chapter-item expanded "><a href="linearizability2.html"><strong aria-hidden="true">5.2.</strong> 线性一致性（二）——细究linearizability</a></li><li class="chapter-item expanded "><a href="zk_intro.html"><strong aria-hidden="true">5.3.</strong> 引言</a></li><li class="chapter-item expanded "><a href="zk_service.html"><strong aria-hidden="true">5.4.</strong> Zookeeper Service</a></li><li class="chapter-item expanded "><a href="zk_api.html"><strong aria-hidden="true">5.5.</strong> Zookeeper API</a></li><li class="chapter-item expanded "><a href="zk_prop.html"><strong aria-hidden="true">5.6.</strong> Zookeeper的性质</a></li><li class="chapter-item expanded "><a href="zk_ex.html"><strong aria-hidden="true">5.7.</strong> 基于Zookeeper实现锁</a></li></ol></li><li class="chapter-item expanded "><a href="CRAQ.html"><strong aria-hidden="true">6.</strong> CRAQ</a></li><li class="chapter-item expanded "><a href="lamport_clock.html"><strong aria-hidden="true">7.</strong> Time, Clocks, and the Ordering of Events in a Distributed System</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="lamport_clock1.html"><strong aria-hidden="true">7.1.</strong> 引言</a></li><li class="chapter-item expanded "><a href="lamport_clock_partial_order.html"><strong aria-hidden="true">7.2.</strong> 偏序关系</a></li><li class="chapter-item expanded "><a href="lamport_logic_clock.html"><strong aria-hidden="true">7.3.</strong> 逻辑时钟</a></li><li class="chapter-item expanded "><a href="lamport_total_order.html"><strong aria-hidden="true">7.4.</strong> 全序关系</a></li><li class="chapter-item expanded "><a href="lamport_clock_ana_behave.html"><strong aria-hidden="true">7.5.</strong> 异常事件</a></li><li class="chapter-item expanded "><a href="lamport_p_clock.html"><strong aria-hidden="true">7.6.</strong> 物理时钟</a></li><li class="chapter-item expanded "><a href="lamport_end.html"><strong aria-hidden="true">7.7.</strong> 结论</a></li></ol></li><li class="chapter-item expanded "><li class="part-title">6.828 操作系统</li><li class="chapter-item expanded "><a href="828intro.html"><strong aria-hidden="true">8.</strong> Killer of Microseconds</a></li><li class="chapter-item expanded "><a href="cloudlab.html"><strong aria-hidden="true">9.</strong> CloudLab</a></li><li class="chapter-item expanded "><a href="dpdk.html"><strong aria-hidden="true">10.</strong> DPDK</a></li><li class="chapter-item expanded "><a href="spdk.html"><strong aria-hidden="true">11.</strong> SPDK</a></li><li class="chapter-item expanded "><a href="Shenango.html"><strong aria-hidden="true">12.</strong> Shenango</a></li><li class="chapter-item expanded "><a href="TritonSort.html"><strong aria-hidden="true">13.</strong> TritonSort</a></li><li class="chapter-item expanded "><a href="Profiling.html"><strong aria-hidden="true">14.</strong> Profiling a warehouse-scale computer</a></li><li class="chapter-item expanded affix "><li class="part-title">6.828 - Network</li><li class="chapter-item expanded affix "><li class="part-title">CS244 - Advanced Topics in Networking</li><li class="chapter-item expanded "><a href="DARPA_NET.html"><strong aria-hidden="true">15.</strong> The Design Philosophy of The DARPA Internet Protocols</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="DARPA_NET2.html"><strong aria-hidden="true">15.1.</strong> Second Level Goals</a></li><li class="chapter-item expanded "><a href="DARPA_NET3.html"><strong aria-hidden="true">15.2.</strong> Types of Service</a></li><li class="chapter-item expanded "><a href="DARPA_NET4.html"><strong aria-hidden="true">15.3.</strong> Varieties of Networks</a></li><li class="chapter-item expanded "><a href="DARPA_NET5.html"><strong aria-hidden="true">15.4.</strong> Architecture and Implementation</a></li><li class="chapter-item expanded "><a href="DARPA_NET6.html"><strong aria-hidden="true">15.5.</strong> Datagrams</a></li></ol></li><li class="chapter-item expanded "><li class="part-title">最后</li><li class="chapter-item expanded "><a href="end.html"><strong aria-hidden="true">16.</strong> 最后</a></li></ol>
            </div>
            <div id="sidebar-resize-handle" class="sidebar-resize-handle"></div>
        </nav>

        <div id="page-wrapper" class="page-wrapper">

            <div class="page">
                                <div id="menu-bar-hover-placeholder"></div>
                <div id="menu-bar" class="menu-bar sticky bordered">
                    <div class="left-buttons">
                        <button id="sidebar-toggle" class="icon-button" type="button" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="sidebar">
                            <i class="fa fa-bars"></i>
                        </button>
                        <button id="theme-toggle" class="icon-button" type="button" title="Change theme" aria-label="Change theme" aria-haspopup="true" aria-expanded="false" aria-controls="theme-list">
                            <i class="fa fa-paint-brush"></i>
                        </button>
                        <ul id="theme-list" class="theme-popup" aria-label="Themes" role="menu">
                            <li role="none"><button role="menuitem" class="theme" id="light">Light (default)</button></li>
                            <li role="none"><button role="menuitem" class="theme" id="rust">Rust</button></li>
                            <li role="none"><button role="menuitem" class="theme" id="coal">Coal</button></li>
                            <li role="none"><button role="menuitem" class="theme" id="navy">Navy</button></li>
                            <li role="none"><button role="menuitem" class="theme" id="ayu">Ayu</button></li>
                        </ul>
                        <button id="search-toggle" class="icon-button" type="button" title="Search. (Shortkey: s)" aria-label="Toggle Searchbar" aria-expanded="false" aria-keyshortcuts="S" aria-controls="searchbar">
                            <i class="fa fa-search"></i>
                        </button>
                    </div>

                    <h1 class="menu-title">读论文</h1>

                    <div class="right-buttons">
                        <a href="print.html" title="Print this book" aria-label="Print this book">
                            <i id="print-button" class="fa fa-print"></i>
                        </a>

                    </div>
                </div>

                <div id="search-wrapper" class="hidden">
                    <form id="searchbar-outer" class="searchbar-outer">
                        <input type="search" id="searchbar" name="searchbar" placeholder="Search this book ..." aria-controls="searchresults-outer" aria-describedby="searchresults-header">
                    </form>
                    <div id="searchresults-outer" class="searchresults-outer hidden">
                        <div id="searchresults-header" class="searchresults-header"></div>
                        <ul id="searchresults">
                        </ul>
                    </div>
                </div>

                <!-- Apply ARIA attributes after the sidebar and the sidebar toggle button are added to the DOM -->
                <script type="text/javascript">
                    document.getElementById('sidebar-toggle').setAttribute('aria-expanded', sidebar === 'visible');
                    document.getElementById('sidebar').setAttribute('aria-hidden', sidebar !== 'visible');
                    Array.from(document.querySelectorAll('#sidebar a')).forEach(function(link) {
                        link.setAttribute('tabIndex', sidebar === 'visible' ? 0 : -1);
                    });
                </script>

                <div id="content" class="content">
                    <main>
                        <h1 id="raft共识算法三日志备份log-replication"><a class="header" href="#raft共识算法三日志备份log-replication">Raft共识算法（三）——日志备份（log replication）</a></h1>
<blockquote>
<p>leader被选出来了，那它该如何响应上层调用者的请求？<br />
另外，如果leader宕机，该如何从崩溃中迅速恢复？</p>
</blockquote>
<p>一旦leader被选举出来了，它就会开始服务client的请求。每一个client发过来的请求都包含了一个复制状态机要去执行的命令。<br />
leader将这个命令看作是一个新的entry，追加到它的log里，然后给每个server并行地发送AppendEntries RPC请求，从而实现日志条目entry的备份。<br />
当这个entry被安全的（我们在下文会定义安全性）复制成功后，leader就会把这个entry添加到它的状态机里并且将它的执行结果返回给client。<br />
<strong>如果follower宕机或者速度太慢，或者发生了网络丢包，leader就会不断地尝试重新发送AppendEntries RPC请求直到follower最终成功地将所有日志条目存储下来。</strong></p>
<p><img src="./assets/raft_f3_3.png" alt="raft_f3_3" /></p>
<p><img src="./assets/raft_f6.png" alt="raft_f6" /></p>
<p>log的组织格式如图6所示。每个来自leader的log entry都会存储一个状态机命令以及它的term值。这个term值用来检测log里的不一致的地方，以及用来确保图3中raft的一些性质。每一个log entry同样有一个整数类型的index号，用来定位它在log中的位置。</p>
<p>leader会判断什么时候给状态机追加（apply）一个entry是安全的，被追加的entry状态我们称之为提交态（committed）。<br />
Raft保证了每一个被apply的日志都是被持久化的，而且其最终会被所有的server状态机所执行。<br />
一旦leader把这个entry备份到过半的follower的状态机里之后，leader就会把这个entry设置为提交态（e.g. 图6中的entry7）。<br />
leader也会将一些它之前的entry进行提交，包括前任leader创建的entry。<br />
5.4节会讨论一些不易察觉的事件，比如说换leader的时候追加日志的规则应该是什么，并且证明这种提交策略是足够安全的。<br />
leader会一直跟踪被提交的entry里最高的那个log index，并且将这个index放到AppendEntries RPC里（包括心跳包），这样的话其他server就能知道目前进行到哪里了。<br />
一旦follower发现有一些entry该变成提交态了，它就会把这些entry按顺序添加到它本地的状态机里。</p>
<p>我们设计的raft机制，使分布在不同server上的logs具有高内聚的特性。<br />
这种机制不但简化了系统的行为且具有良好的可预测性，更重要的是它安全。</p>
<p>Raft会一直保证它的如下两个性质，这两个性质共同组成了图3所说的（日志匹配特性）“The Log Matching Property”：</p>
<ul>
<li>如果两个entry在不同server的日志里具有相同的index和term，那它们存储的命令一定是相同的。</li>
<li>如果两个entry在不同server的日志里具有相同的index和term，那它们各自的日志里之前所有的log entries均相同。</li>
</ul>
<p>第一个性质基于如下事实：leader创建entry时，会给定一个log index和term，log entry永远不会改变它在log里的位置。<br />
而AppendEntries RPC里简单的一致性检查则保证了第二个性质，通过比对log index和term，follower可以立即着手处理最新的entry。<br />
如果follower在日志里并没有发现与之匹配的index和term，它会拒绝掉这条最新的entry。<br />
这种一致性检查基于如下推论：<br />
最初的空log满足日志匹配特性，而只有当追加的entry还能继续满足日志匹配性的时候，才可以进行追加操作。（有点像是数学归纳法）<br />
因此，每当AppendEntries返回成功的时候，leader就能确认follower的log和它自己的保持一致了。</p>
<p><img src="./assets/raft_7.png" alt="raft_7" />
<em>图7:当leader拿到话事权时，follower可能会出现如下从a到f的情况。
每个长条表示一个log，长条里的box代表了一个日志entry；box里的值表示term。
一个follower可能会丢失entry（a和b），可能会有额外未提交的entry（c-d），抑或是兼而有之（e-f）。
比如说，我们来看一种f情况出现的一种场景：如果一个server在term2的时候是一个leader，它将若干entry添加到自己的log中，然后它在未提交的情况下宕机了；
假设它迅速地重启，并成为了term3的leader，又往自己的log里添加了若干entry；
假设它又没来得及提交term2和term3里的条目，就在接下来的几个term里一直宕机，f的这种情况就会出现。</em></p>
<p>在正常的操作中，leader的log和follower的会保持一致，因此AppendEntries的一致性检查就永远不会出错。然而，leader宕机的时候会导致log不一致的情况出现（比如说老leader还没有完全地把entry都备份到日志里面）。<br />
这种不一致会造成一系列的leader、follower的宕机行为。</p>
<p>如图7所示，可能会有follower的日志和新leader的日志不一样的情况。follower拥有的日志条目可能会少于leader的，也有可能会多于leader的，或者这种情况兼而有之。<br />
这种丢entry或者多entry的情况，在每一个term里都有可能会出现。</p>
<p>在raft协议中，leader处理不一致的措施是：强迫follower的log与leader保持一致。这意味着follower中冲突的entry会被leader里的entry所重写。5.4小节会说明这种方式和一种更严格的限制措施结合之后，是可以保证数据安全性的。</p>
<p>作为leader，为了使follower的log与其保持一致，leader必须必须找到集群中最新的entry，确定大家的当前工作进度到哪一步了，然后删除follower里这个工作点往后的log entries。
并且基于它自己的log，把leader里的这个entry之后的所有entry再次发给所有的follower。<br />
所有的这些操作都是由AppendEntires的一致性检查完成的。leader为每一个follower都维护一个nextIndex，即要发给follower的下一个entry的索引值。<br />
当leader最开始拿到话事权的时候，它会把所有的follower对应的nextIndex初始化成它自己log里最新的那一条（在图7中，index的值就是11）。如果follower的log与leader的不符，下一次的AppendEntries请求就通不过一致性检查。于是follower会拒绝AppendEntries的请求，leader就会把nextIndex的值递减，并再次尝试发送AppendEntries。最终leader和follower的log会保持一致。<br />
一致的情况意味着，AppendEntries请求会成功返回，并且follower里冲突的entry会被移除掉，并且会新增上leader里log的entry（如果有的话）。一旦AppendEntries成功，follower的日志就和leader保持一致，然后它们就可以在这一轮term愉快地工作了。</p>
<p>如果有额外的性能需求，raft协议可以通过减少拒绝掉AppendEntries的次数来进行优化。比如说，在拒绝AppendEntries请求的时候，follower可以在返回的结构体里面放入冲突的entry以及本轮term里它存的第一个日志条目的index。有了这些信息，leader就能绕过冲突的条目，直接将nextIndex的值递减到follower发过来的index上；一个AppendEntries请求可以把所有冲突的entry传进去，而不是单个RPC只包含一个entry。具体实现的时候，我们其实对这种优化的必要性存疑，因为failure实在太不常见了，所以不会有太多不一致的entry条目。</p>
<p>使用如此机制，leader在拿到话事权的时候，不需要对log的一致性做过多的特定的操作。它只需要正常操作即可，log会在AppendEntries的一致性检查中，逐渐趋向一致。一个leader永远不会覆写或者删除它自己的log（图3中有提到，Leader Append-Only Property特性，即leader只做追加操作的原则）。</p>
<p>日志复制机制体现了我们在第二章所提到的共识算法的一致性：</p>
<ul>
<li>当新的entries被过半的server所接受，raft服务器可以接收、复制、添加新的entry到它的log中；</li>
<li>大多数正常的情况下，一个entry大概会经过一轮RPC的时间，从而被集群server备份；</li>
<li>而单个处理时间缓慢的follower不会影响整体系统的性能。</li>
</ul>
<hr />
<blockquote>
<p><strong>额外的话</strong>：<br />
看这段的时候，心生了很多问题。</p>
<ul>
<li>
<p><strong>如果正常工作则万事大吉，万一崩溃了，如何处理日志不一样的情况呢？</strong><br />
论文里说，通过AppendEntries的一致性检查，使日志逐渐趋于一致。这个需要我们仔细地进行设计。</p>
</li>
<li>
<p><strong>另一个问题是，会不会存在follower里的日志比leader的日志要长的情况？</strong><br />
答案是会，因此论文又增加了选举限制规则，进一步保证日志的一致性。关于选举限制规则，会在后面的两节出现。</p>
</li>
<li>
<p><strong>此外，关于leader和follower通过AppendEntries同步log的时候，如何优化性能？</strong><br />
我没有采取论文和824的lecture里提到的方法，而是采用每次将差值*2的方式进行同步，这个方法同样通过了lab2的性能测试，且实现起来更加简单。</p>
</li>
</ul>
<p>有了这些信息，我们就可以去做6.824的lab2b了。</p>
</blockquote>

                    </main>

                    <nav class="nav-wrapper" aria-label="Page navigation">
                        <!-- Mobile navigation buttons -->
                            <a rel="prev" href="Raft7.html" class="mobile-nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
                                <i class="fa fa-angle-left"></i>
                            </a>

                            <a rel="next" href="Raft9.html" class="mobile-nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
                                <i class="fa fa-angle-right"></i>
                            </a>

                        <div style="clear: both"></div>
                    </nav>
                </div>
            </div>

            <nav class="nav-wide-wrapper" aria-label="Page navigation">
                    <a rel="prev" href="Raft7.html" class="nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
                        <i class="fa fa-angle-left"></i>
                    </a>

                    <a rel="next" href="Raft9.html" class="nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
                        <i class="fa fa-angle-right"></i>
                    </a>
            </nav>

        </div>




        <script type="text/javascript">
            window.playground_copyable = true;
        </script>


        <script src="elasticlunr.min.js" type="text/javascript" charset="utf-8"></script>
        <script src="mark.min.js" type="text/javascript" charset="utf-8"></script>
        <script src="searcher.js" type="text/javascript" charset="utf-8"></script>

        <script src="clipboard.min.js" type="text/javascript" charset="utf-8"></script>
        <script src="highlight.js" type="text/javascript" charset="utf-8"></script>
        <script src="book.js" type="text/javascript" charset="utf-8"></script>

        <!-- Custom JS scripts -->


    </body>
</html>
